package kerr

import (
	"errors"
	"fmt"
	"path"
	"runtime"
	"strings"
)

// TraceStack 堆栈信息
type TraceStack struct {
	FuncName string
	FileName string
	Line     int
}

func (t *TraceStack) String() string {
	return fmt.Sprintf("%s: %s:%d", t.FuncName, t.FileName, t.Line)
}

// traceStackError 错误堆栈信息
type traceStackError struct {
	err          error
	stacks       []TraceStack
	outputStacks bool
}

func (t *traceStackError) Error() string {
	if !t.outputStacks {
		return t.err.Error()
	}
	buf := new(strings.Builder)
	buf.WriteString(t.err.Error())
	buf.WriteString("\n")
	for _, stack := range t.stacks {
		buf.WriteString(stack.String())
		buf.WriteString("\n")
	}
	return buf.String()
}

func (t *traceStackError) Unwrap() error {
	return t.err
}

// WrapTraceStack 包装堆栈信息到错误中.
// 输出错误时会不会携带堆栈信息,
// 需要输出错误带上堆栈信息请使用 WithTraceStack.
// 该方法的堆栈信息需要使用 GetTraceStack 获取.
func WrapTraceStack(err error) error {
	if err == nil {
		return nil
	}
	tsErr, ok := err.(*traceStackError)
	if ok {
		return err
	}
	tsErr = &traceStackError{
		err: err,
	}
	skip := 1
	for {
		pc, file, line, ok := runtime.Caller(skip)
		if !ok {
			break
		}
		fn := runtime.FuncForPC(pc)
		if fn == nil {
			break
		}
		tsErr.stacks = append(tsErr.stacks, TraceStack{
			FuncName: path.Base(fn.Name()),
			FileName: file,
			Line:     line,
		})
		skip++
	}
	return tsErr
}

// GetTraceStacks 获取错误中的堆栈信息.
func GetTraceStacks(err error) []TraceStack {
	tsErr, ok := err.(*traceStackError)
	if ok {
		return tsErr.stacks
	}
	return nil
}

// WithTraceStack 输出错误时带上堆栈信息.
func WithTraceStack(err error) error {
	if err == nil {
		return nil
	}
	tsErr, ok := err.(*traceStackError)
	if ok {
		if tsErr.outputStacks {
			return err
		}
		return &traceStackError{
			err:          tsErr.err,
			stacks:       tsErr.stacks,
			outputStacks: true,
		}
	}
	tsErr = &traceStackError{
		err:          err,
		outputStacks: true,
	}
	skip := 1
	for {
		pc, file, line, ok := runtime.Caller(skip)
		if !ok {
			break
		}
		fn := runtime.FuncForPC(pc)
		if fn == nil {
			break
		}
		tsErr.stacks = append(tsErr.stacks, TraceStack{
			FuncName: path.Base(fn.Name()),
			FileName: file,
			Line:     line,
		})
		skip++
	}
	return tsErr
}

func New(text string) error {
	tsErr := &traceStackError{
		err: errors.New(text),
	}
	skip := 1
	for {
		pc, file, line, ok := runtime.Caller(skip)
		if !ok {
			break
		}
		fn := runtime.FuncForPC(pc)
		if fn == nil {
			break
		}
		tsErr.stacks = append(tsErr.stacks, TraceStack{
			FuncName: path.Base(fn.Name()),
			FileName: file,
			Line:     line,
		})
		skip++
	}
	return tsErr
}
